Scala a functional programming approach 1: Pure functions

Abstract

This document is a efford to introduce the strengths and benefits of functional programming in scala.

We do not claim intellectual property of all the material presented. We specifically refer to the original resources whenever is needed.

The presentation path of the concepts is still under consideration and may be changed in future reviews.

Outline

This notebooks covers:

  • Pure function basics.
  • Function composition.
  • Understanding side effects.
  • Refactoring effects.
  • Introduction of Option data type.
  • Basic introduction to scala syntax and semantics.

Pure Functions

resource: Scala IO 2013, Purely functional I/O in Scala, R.O. Bjarnason

A pure function of type A => B takes an argument of type A and returns a value of type B.

And does nothing else

A pure function always returns the same value given the same arguments.

A pure function has no dependencies other than its arguments.

The result of calling a pure function can be understood completely by looking at the returned value.


In [ ]:
// This is how we define method in scala
def inc(x: Int): Int = { x + 1 }

//This is how we define an (anonymous) function
(x: Int) => { x + 1 }

//We can give a name to an anonymous function by assigning it to a variable
val incAnon: Int => Int = (x: Int) => { x + 1 }

// In scala methods are not exactly the same with functions
// We can convert a method to a funciton via an `_`
inc _

def sample(f: Int => Int, x :Int): Int = {
  f(x)
}

// For the purposes of this presentation we will ignore this technicality

In [ ]:
//Let's define some pure functions (methods)

def inc(x: Int): Int = x + 1

def double = (x: Int) => x * 2 // We can omit result type (= scala local type inference)

def stringify (x: Int): String = x.toString()

In [ ]:
// The functions below are impure - why?
def printInt(x: Int): Unit = { 
  println(x)
} 

def divideTenBy(x: Double): Double = 10 / x  
 
def allowOnlyPositive(x: Int):Int = if (x > 0) x else sys.error("only positive numbers")

We can call evaluate/execute without fear the pure functions but what about the impure ones?


In [ ]:
inc(1)
double(2)
stringify(2)

//With impure functions but things happen
divideTenBy(2)
divideTenBy(0) // Not ok!

Impure functions are not so cool...

How would you test function printInt?

The call of divideTenBy(0) gives infinity (???).

Also the call of allowOnlyPositive(0) causes a RuntimeException.


In [ ]:
allowOnlyPositive(-1) // Not ok!

In [ ]:
//((A => B), (B => C)) => (A => C) :: andThen  andThen((inc _),(allowOnlyPositive))

// inc o double 
val incAfterDouble = (inc _).compose(double) // inc(double(x))

def s2(x:Int) = inc(double(x)) 
s2(10)

val incAndThenAllowOnlyPositive = inc _ andThen allowOnlyPositive 

incAfterDouble(10)
// incAndThenAllowOnlyPositive(-2)

Composability

Impurity effects also the composition of functions.

Composing pure functions results in a pure function.

Composing at least one impure function results in an impure function.

Purity and real world

So far so good, function purity is desired in programs, but the reality is devastating. All the programs that matter to us "contain" side effects.

So is this introduction all in vain?

Purity matters even if the overall execution of our programs is impure. What we actually desire is to separate the impure parts of our programs from the pure ones. That means that we write the "business logic" or "logic" of our program in pure functions and hand the results to impure functions to do the "dirty work". In our context the "dirty work" is side effects of all kinds.

Example: Refactoring effects.

Resource: Functional programming in scala: Chapter 13

Consider fragment of code below which calculates the winner of a contest:


In [ ]:
// Program #1 
// Decide the winner of a contest.
case class Player(name: String, score: Int)

def contest(p1: Player, p2: Player): Unit = {
  if(p1.score > p2.score)
    println(s"${p1.name} wins!")
  else if(p2.score > p1.score)
    println(s"${p2.name} wins!")
  else
    println("It is a draw!")
}

// Who is the winner?
val p1 = Player("fpas", 10)
val p2 = Player("gsmyrn", 10)

contest(p1,p2)

We are done right? The program works and prints the correct results.

This program, though simple, has some flows. One of them is that it cannot be tested (easily). This is because the the actual "logic" of computing the winner is interleaved with the mechanism of printing the result.

Let's change this.


In [ ]:
// Program #2
// Split the logic from the effect.
case class Player(name: String, score: Int)

def computeWinner(p1: Player, p2: Player): Player = {
  if(p1.score > p2.score) p1
  else p2 
}

def displayWinner(p: Player): Unit = {
 println(s"${p.name} wins!")
}

//This is function composition
def contest(p1: Player, p2: Player) = displayWinner(computeWinner(p1,p2))

// Who is the winner?
contest(Player("fpas", 10), Player("gsmyrn", 5))

With this simple refactor the contest function is a composition of two other functions. The first part is a pure function containing the logic and the second part is an impure function that dispays the results. The overall contest function is impure.

Take away 1

Given an impure function of type f: A => B we can split into to other functions.

  • A pure function of type A => D where D describes the result of f.
  • An impure function of type D => B, which interprets D and executes the result.

That is what we strive to do in functional programming. Describe all the operations of a program in composed pure functions and promote/push/delay the (side) effects execution to the end of the "chain".

The carefull reader, by now, will have discover an incosistency between Program 1 and Program 2. The wo programs are not equivalent.

What happened to the draw case in Program 2? This was intentional and the inconsistency is caused by the fact that our pure function computeWinner cannot handle the case of a draw.

It cannot handle it because there is no obvious result of type Player to return in the case of scores equality.

A closer look at side effects.

So a pure function such as computeWinner cannot handle with the cases where there is no winner.

This is natural because a pure function must define an output for each given input.

But in our case we want the function to handle cases of player pairs where there is no output (winner).


In [ ]:
// Program #3 
// A better computeWinner

def computeWinner(p1: Player, p2: Player): Player = {
  if(p1.score > p2.score) p1
  else if (p2.score > p1.score) p2
  else null //null is a special value for this function that denotes that there is no winner.
}

Problem solved ! The function remains pure ( null is value of type Player ). Let's try it !


In [ ]:
//[Program 3 - continue

def displayWinner(p: Player): Unit = {
 println(s"${p.name} wins!")
}

//This is function composition
def contest(p1: Player, p2: Player) = displayWinner(computeWinner(p1,p2))


// Who is the winner?
contest(Player("fpas", 10), Player("gsmyrn", 10))

Not what we expected, a NullPointerException. This happens because displayWinner must learn to handle null values of players.

We can do that!

But wait, do we want to do that? Is there a better way?

We don't want to handle null cases in our code like that. null leads to hidden side effects like exceptions.

We want the end user of the pure function computeWinner to have a good description type for its result.

So the correct question is:

Which data type is appropriate to describe the side effect of partial functions?

An alternative more functional approach to the "partial function" side effect problem is the Option[A] type.


In [ ]:
// Program #4
// computeWinner with Option[A]
case class Player(name: String, score: Int)

def computeWinner(p1: Player, p2: Player): Option[Player] = {
  if(p1.score > p2.score) Some(p1) //:Option[Player]
  else if (p2.score > p1.score) Some(p2)
  else None //: Option[Player]
}


def displayWinner(op: Option[Player]): Unit = {
  if(op.isEmpty)  
    println(s"It's a draw")
  else 
    println(s"${op.get.name} wins!")
}


// This is the alternative with pattern matching

// def displayWinner(p: Option[Player]): Unit = p match { //Pattern match instead of 'if' construct
//  case Some(x) => println(s"${x.name} wins!")
//  case None => println(s"It's a draw")
// }

//This is function composition
def contest(p1: Player, p2: Player) = displayWinner(computeWinner(p1,p2))

// Who is the winner? (handles draw)
contest(Player("fpas", 5), Player("gsmyrn", 5))

Program 4 implementation is real close to Program 3 implementation which used null in place of None.

The key difference is that according to Take away 1 we realized that the pure function f: A =>D ( computeWinner) used a data type D (Player) that was not expressive enough to describe the side effect of "partial function"

So we chose an "embelised type" to describe the absense of return value at some cases.

Note, that now every one that reads the signature

def computeWinner(p1: Player, p2: Player): Option[Player]

knows that the method does something that may or may not return a value. The side effect is described and is not hidden as in the case of null usage, or any other programming convention.

Exercise 1: Rewrite contest function using andThen composition function.

Hint use Function1.curried method


In [ ]:
//This is function composition
// def contest(p1: Player, p2: Player) = displayWinner(computeWinner(p1,p2))

def contest(p1: Player, p2:Player) = ???
contest(Player("fpas", 5), Player("gsmyrn", 5))

Exercise 2: There is a new requirement to CAPITALIZE the names of the winners.

a) Create a new method that capitalize a player name.

b) Compose in a functional way the new function into the program.

Hint use option map method.


In [ ]:
// Exercise #2

case class Player(name: String, score: Int)

def computeWinner(p1: Player, p2: Player): Option[Player] = {
  if(p1.score > p2.score) Some(p1)
  else if (p2.score > p1.score) Some(p2)
  else None
}

//Player => Player
def capitalize(p: Player): Player = Player(p.name.toUpperCase, p.score)

def displayWinner(p: Option[Player]): Unit = p match {
 case Some(p) => println(s"${p.name} wins!")
 case None => println(s"It's a draw")
}
  
//This is function composition
def contest(p1: Player, p2: Player): Unit  = displayWinner(computeWinner(p1,p2).map(capitalize)) 
   

contest(Player("fpas", 10), Player("gsmyrn", 10))

Exercise 3: There is a new requirement

If the winner of a contest has a negative or zero score, then the result should be 'a draw'.

Implement a method that checks the winners score.

Hint use option flatMap method


In [ ]:
// Exercise 3
case class Player(name: String, score: Int)

def computeWinner(p1: Player, p2: Player): Option[Player] = {
  if(p1.score > p2.score) Some(p1)
  else if (p2.score > p1.score) Some(p2)
  else None
}

def capitalize(p: Player): Player = p.copy(name = p.name.toUpperCase)

def checkScore(p: Player): Option[Player] = ???

def displayWinner(p: Option[Player]): Unit = p match { 
 case Some(p) => println(s"${p.name} wins!")
 case None => println(s"It's a draw")
}


//This is function composition
def contest(p1: Player, p2: Player) = ???

contest(Player("fpas", 0), Player("gsmyrn",-15))

Conclusion

  • Scala is an expressive language that is able to support functional programming paradigm.

  • In functional programming paradigm we try to describe with higher constructs the execution of programs.

  • We also try to handle side effects using contained and controlled parts of the program that interpret how to execute the side effects.

  • Side effects are described in functional programming using more expressive types ("embelished types").

  • Functional programming tries to promote Composability as a mean to achieve all the above goals.

    Finally, functional programming requires a paradigm shift

    FROM writing programs that do operations TO writing programs that describe oparations

Fotios Paschos, @fpaschos, Sep 2017